<?php

require_once 'Rfc3986.php';
require_once 'OAuthException.php';

/**
 * Class OAuthRequest
 * Edit from original library OAuth-PHP of Marc Worrell <marcw@pobox.com>
 *
 * @editor: Phong Linh <langxangvn@gmail.com>
 * @see http://code.google.com/p/oauth-php
*/
class OAuthRequest {
	/* [ API PARAMETERS CONSTANTS ] */
	/* List of know and used API parameters' names */
	
	/**
	* OAuth parameter prefix: oauth_
	*/
	public static $OAUTH_PARAM_PREFIX = "oauth_";
	
	/**
	* XOAuth parameter prefix: xoauth_
	*/
	public static $XOAUTH_PARAM_PREFIX = "xoauth_";
	
	/**
	* XAuth parameter prefix: xauth_
	*/
	public static $X_AUTH_PARAM_PREFIX = "xauth_";
	
	/**
	* XAuth mode: x_auth_mode
	*/
	public static $X_AUTH_MODE  = "x_auth_mode";
	
	/**
	* XAuth password: x_auth_password
	*/
	public static $X_AUTH_PASSWORD = "x_auth_password";
	
	/**
	* XAuth user name: x_auth_username
	*/
	public static $X_AUTH_USERNAME  = "x_auth_username";
	
	/**
	* XOAuth token TTL: xoauth_token_ttl
	*/
	public static $XOAUTH_TOKEN_TTL = "xoauth_token_ttl";
	
	/**
	* XOAuth body signature: xoauth_body_signature
	*/
	public static $XOAUTH_BODY_SIGNATURE = "xoauth_body_signature";
	
	/**
	* XOAuth body signature method: xoauth_body_signature_method
	*/
	public static $XOAUTH_BODY_SIGNATURE_METHOD = "xoauth_body_signature_method";
	
	/**
	* XOAuth lang pref: xoauth_lang_pref
	*/
	public static $XOAUTH_LANG_PREF = "xoauth_lang_pref";
	
	/**
	* XOAuth request auth URL: xoauth_request_auth_url
	*/
	public static $XOAUTH_REQUEST_AUTH_URL = "xoauth_request_auth_url";
	
	/**
	 * OAuth consumer key: oauth_consumer_key
	 */
	public static $OAUTH_CONSUMER_KEY = "oauth_consumer_key";
	
	/**
	 * OAuth consumer key: oauth_consumer_secret
	 */
	public static $OAUTH_CONSUMER_SECRET = "oauth_consumer_secret";
	
	/**
	 * OAuth token key: oauth_token
	 */
	public static $OAUTH_TOKEN = "oauth_token";
	
	/**
	 * OAuth token secret: oauth_token_secret
	 */
	public static $OAUTH_TOKEN_SECRET = "oauth_token_secret";
	
	/**
	 * OAuth expires in: oauth_expires_in
	 */
	public static $OAUTH_EXPIRES_IN = "oauth_expires_in";
	
	/**
	 * OAuth signature method: oauth_signature_method
	 */
	public static $OAUTH_SIGNATURE_METHOD = "oauth_signature_method";
	
	/**
	 * OAuth signature: oauth_signature
	 */
	public static $OAUTH_SIGNATURE = "oauth_signature";
	
	/**
	 * OAuth timestamp: oauth_timestamp
	 */
	public static $OAUTH_TIMESTAMP = "oauth_timestamp";
	
	/**
	 * OAuth nonce: oauth_nonce
	 */
	public static $OAUTH_NONCE = "oauth_nonce";
	
	/**
	 * OAuth callback: oauth_callback
	 */
	public static $OAUTH_CALLBACK = "oauth_callback";
	
	/**
	 * OAuth callback: oauth_callback_confirmed
	 */
	public static $OAUTH_CALLBACK_CONFIRMED = "oauth_callback_confirmed";
	
	/**
	 * OAuth verifier: oauth_verifier
	 */
	public static $OAUTH_VERIFIER = "oauth_verifier";		
	
	/**
	 * OAuth version: oauth_version
	 */
	public static $OAUTH_VERSION = "oauth_version";		
	
	/**
	 * OAuth version: oauth_version
	 */
	public static $API_FORMAT = "format";
	
	/* [ END API PARAMETERS CONSTANTS ] */
	
	/**
	* The realm for this request
	*/
	protected $_realm;	
	
	/**
	 * All the parameters, RFC3986 encoded name/value pairs
	 */
	protected $_params = array();
	
	/**
	* The request raw URI
	*/
	protected $_uri;
	
	/**
	* The request URI components
	*/
	protected $_uriComponents;	
	
	/**
	* The request headers
	*/
	protected $_headers;
	
	/**
	* The request HTTP method
	*/
	protected $_method;
	
	/**
	 * The body of the OAuth request
	 */	
	protected $_body;
	
	/**
	 * Construct from the current request. Useful for checking the signature of a request.
	 * When not supplied with any parameters this will use the current request.
	 *
	 * @param string $uri URI
	 * @param string $method GET, PUT, POST etc.
	 * @param string $params additional post parameters as string
	 * @param array $headers headers for request
	 * @param string $body optional body of the request (POST or PUT)
	 *
	 */
	public function OAuthRequest($uri = null, $method = null, 
		$params = '', $headers = array(), $body = null)
	{		
		if (is_object($_SERVER)) {
			if (!$method) {		// tainted arrays - the normal stuff in anyMeta
				$method = $_SERVER->REQUEST_METHOD->getRawUnsafe();
			}
			
			if (empty($uri)) {
				$uri    = $_SERVER->REQUEST_URI->getRawUnsafe();
				$proto = (isset($_SERVER['HTTPS']) && $_SERVER['HTTPS'] == 'on') ? 'https' : 'http';
				if (strpos($uri, "://") === false) {
					$uri = sprintf('%s://%s%s', $proto, $_SERVER->HTTP_HOST->getRawUnsafe(), $uri);
				}
			}
		} else {
			if (!$method) {		// non anyMeta systems
				if (isset($_SERVER['REQUEST_METHOD'])) {
					$method = $_SERVER['REQUEST_METHOD'];
				} else {
					$method = 'GET';
				}
			}

			$proto = (isset($_SERVER['HTTPS']) && $_SERVER['HTTPS'] == 'on') ? 'https' : 'http';
			if (empty($uri)) {
				if (strpos($_SERVER['REQUEST_URI'], "://") !== false) {
					$uri = $_SERVER['REQUEST_URI'];
				}
				else {
					$uri = sprintf('%s://%s%s', $proto, $_SERVER['HTTP_HOST'], $_SERVER['REQUEST_URI']);
				}
			}
		}
		
		$headers = getAllHeaders();
		$method = strtoupper($method);
		
		// set method here, read request body require method member
		$this->_method  = $method;		// fixed can not read request body 2012/03/27
		
		// if this is a post then also check the posted variables
		if (strcasecmp($method, 'POST') == 0) {
			if ($this->getRequestContentType() == 'multipart/form-data') {	// what to do with 'multipart/form-data'?				
				if (!isset($headers['X-OAuth-Test'])) {		// get the posted body (when available)
					$params .= $this->getRequestBodyOfMultipart();
				}
			}
			
			if ($this->getRequestContentType() == 'application/x-www-form-urlencoded') {				
				if (!isset($headers['X-OAuth-Test'])) {		// get the posted body (when available)
					$params .= $this->getRequestBody();
				}
			} else {
				$body = $this->getRequestBody();
			}
		} else if (strcasecmp($method, 'PUT') == 0) {
			$body = $this->getRequestBody();
		}		
		
		// header
		$this->_headers = $headers;
		
		// store value, prepare for verify
		$this->_uri = $uri;
		$this->_body = $body;
		
		// parse URI with optional parameters string
		$this->parseUri($params);
		$this->parseHeaders();
		
		// re-encode RFC 3986 after get all params
		$this->transcodeParams();		
	}
	
	/**
	* Helper to try to sort out headers for people who aren't running apache,
	* or people who are running PHP as FastCGI.
	*
	* @return array of request headers as associative array.
	*/
	public static function getAllHeaders() {
		$result = array();
		$headers = array();
		
		if (function_exists('apache_request_headers')) {
			$headers = apache_request_headers();
		} else {
			$headers = array_merge($_ENV, $_SERVER);			
			foreach ($headers as $key => $val) {				
				if (strpos(strtolower($key), 'content-type') !== FALSE)	{	//we need this header
					continue;
				}
				
				if (strtoupper(substr($key, 0, 5)) != "HTTP_") {
					unset($headers[$key]);
				}
			}
		}
		
		// normalize this array to Cased-Like-This structure.
		foreach ($headers AS $key => $value) {
			$key = preg_replace('/^HTTP_/i', '', $key);
			$key = str_replace(
				" ", "-",
				ucwords(strtolower(str_replace(array("-", "_"), " ", $key)))
			);
			$result[$key] = $value;
		}
		ksort($result);
		
		return $result;
	}
	
	/**
	 * This is method getMethod
	 *
	 * @return string Return HTTP method of this request
	 *
	 */
	public function getMethod()	{
		return $this->_method;	
	}
	
	/**
	 * Get the body of this request
	 *
	 * @return string The request's body
	 *
	 */
	public function getBody() {
		return $this->_body;	
	}		
	
	/**
	 * Set the body for this request
	 *
	 * @param string $body Null if not available
	 *
	 */
	public function setBody($body) {
		$this->_body = $body;	
	}	
	
	/**
	 * Return the normalized URL for signature checks
	 */
	public function getRequestUrl() {
		// URI components has been parsed in constructor		
		$url =  $this->_uriComponents['scheme'] . '://'
			. $this->_uriComponents['user'] . (!empty($this->_uriComponents['pass']) ? ':' : '')
			. $this->_uriComponents['pass'] . (!empty($this->_uriComponents['user']) ? '@' : '')
			. $this->_uriComponents['host'];
		
		if ($this->_uriComponents['port'] && 
				$this->_uriComponents['port'] != $this->getDefaultPort($this->_uriComponents['scheme'])) 
		{
			$url .= ':' . $this->_uriComponents['port'];
		}
		if (!empty($this->_uriComponents['path'])) {
			$url .= $this->_uriComponents['path'];
		}
		
		return $url;
	}
	
	/**
	 * Get a parameter, value is default URL encoded
	 *
	 * @param string $name Parameter's name
	 * @param boolean $encode Set to true to decode the value upon return
	 * @return string False when not found
	 *
	 */
	public function getParam($name, $decode = false) {
		if (isset($this->_params[$name])) {
			$value = $this->_params[$name];
		} elseif (isset($this->_params[Rfc3986::urlEncode($name)])) {
			$value = $this->_params[Rfc3986::urlEncode($name)];
		} else {
			$value = false;
		}
		
		if (!empty($value) && $decode) {
			if (is_array($value)) {
				$value = array_map(array('Rfc3986', 'urlDecode'), $value);
			} else {
				$value = Rfc3986::urlDecode($value);
			}
		}
		return $value;		
	}
	
	/**
	 * Set a parameter
	 *
	 * @param string $name The parameter name
	 * @param string $value The value
	 * @param boolean $encoded Set encoded or not both name and value. Default is false.
	 *
	 */
	function setParam($name, $value, $encoded = false) {
		if (!$encoded) {
			$nameEncoded = Rfc3986::urlEncode($name);
			if (is_array($value)) {
				foreach ($value as $v) {
					$this->_params[$nameEncoded][] = Rfc3986::urlEncode($v);
				}
			} else {
				$this->_params[$nameEncoded] = Rfc3986::urlEncode($value);
			}
		} else {
			$this->_params[$name] = $value;
		}
	}
	
	/**
	 * Re-encode all parameters so that they are encoded using RFC 3986.
	 * Updates the $this->_params attribute.
	 *
	 */
	protected function transcodeParams() {
		$params = $this->_params;
		$transcode = array();
		
		foreach ($params as $name => $value) {
			if (is_array($value)) {
				$transcode[Rfc3986::urlTranscode($name)] = array_map(array('Rfc3986', 'urlTranscode'), $value);
			} else {
				$transcode[Rfc3986::urlTranscode($name)] = Rfc3986::urlTranscode($value);
			}
		}
		
		$this->_params = $transcode;
	}	
	
	/**
	* Fetch the content type of the current request
	* 
	* @return string The request content-type
	*/
	protected function getRequestContentType() {
		$type = 'application/octet-stream';
		if (!empty($_SERVER) && array_key_exists('CONTENT_TYPE', $_SERVER)) {
			list($type) = explode(';', $_SERVER['CONTENT_TYPE']);
		}
		
		return trim($type);
	}	
	
	/**
	 * Get the body of a POST or PUT.
	 * Used for fetching the post parameters and to calculate the body signature.
	 *
	 * @return string Return null when no body present (or wrong content type for body)
	 *
	 */
	protected function getRequestBody() {
		$body = null;
		
		if ($this->_method == 'POST' || $this->_method == 'PUT') {
			$body = '';
			$fh   = @fopen('php://input', 'r');
			if ($fh) {
				while (!feof($fh)) {
					$chunk = fread($fh, 1024);
					if (is_string($chunk)) {
						$body .= $chunk;
					}
				}
				
				fclose($fh);
			}
		}
		
		return $body;
	}	
	
	/**
	 * Get the body of a POST with multipart/form-data by Edison tsai on 16:52 2010/09/16
	 *
	 * Used for fetching the post parameters and to calculate the body signature.
	 *
	 * @return string null when no body present (or wrong content type for body)
	 */
	protected function getRequestBodyOfMultipart() {
		$body = null;
		if ($this->_method == 'POST') {
            $body = '';
            if (is_array($_POST) && count($_POST) > 1) {
                foreach ($_POST AS $k => $v) {
					$body .= $k . '=' . Rfc3986::urlEncode($v) . '&';
                }
					
				if(substr($body,-1) == '&') {
					$body = substr($body, 0, strlen($body) - 1);
				}
            }
		}

		return $body;
	}
	
	/**
	 * Simple function to perform a redirect (GET).
	 * Redirects the User-Agent, does not return.
	 *
	 * @param string $uri URI to redirect to
	 * @param array $params Parameters with URL encoded
	 * @exception Exception Throw exception when redirect URI is illegal
	 *
	 */
	public function redirect($uri, $params) {
		if (!empty($params)) {
			$list = array();
			foreach ($params as $name => $value) {
				$list[] = $name . '=' . $value;
			}
			$query = implode('&', $list);
			
			if (strpos($uri, '?')) {
				$uri .= '&' . $query;
			} else {
				$uri .= '?' . $query;
			}
		}
		
		// simple security - multiline location headers can inject all kinds of extras
		$uri = preg_replace('/\s/', '%20', $uri);
		if (strncasecmp($uri, 'http://', 7) && strncasecmp($uri, 'https://', 8)) {
			if (strpos($uri, '://')) {
				throw new OAuthException('Illegal protocol in redirect uri ' . $uri);
			}
			$uri = 'http://' . $uri;
		}
		
		header('HTTP/1.1 302 Found');
		header('Location: ' . $uri);
		echo '';
		exit();
	}	
	
	/**
	 * Parse the oauth parameters from the request headers
	 * Looks for something like:
	 *
	* Authorization: OAuth realm="http://photos.example.net/authorize",
	*           oauth_consumer_key="dpf43f3p2l4k3l03",
	*           oauth_token="nnch734d00sl2jdk",
	*           oauth_signature_method="HMAC-SHA1",
	*           oauth_signature="tR3%2BTy81lMeYAr%2FFid0kMTYa%2FWM%3D",
	*           oauth_timestamp="1191242096",
	*           oauth_nonce="kllo9940pd9333jh",
	*           oauth_version="1.0"
	     */	
	protected function parseHeaders() {
		/*
			$this->headers['Authorization'] = 'OAuth realm="http://photos.example.net/authorize",
			oauth_consumer_key="dpf43f3p2l4k3l03",
			oauth_token="nnch734d00sl2jdk",
			oauth_signature_method="HMAC-SHA1",
			oauth_signature="tR3%2BTy81lMeYAr%2FFid0kMTYa%2FWM%3D",
			oauth_timestamp="1191242096",
			oauth_nonce="kllo9940pd9333jh",
			oauth_version="1.0"';
		*/ 	
		
		if (isset($this->_headers['Authorization'])) {
			$auth = trim($this->_headers['Authorization']);
			if (strncasecmp($auth, 'OAuth', 4) == 0) {
				$pairs = explode(',', substr($auth, 6));
				foreach ($pairs as $pair) {
					if (strpos($pair, '=')) {
						$pair = trim($pair);
						@list($name, $value) = explode('=', $pair, 2);
						
						if (!empty($value) && $value{0} == '"' && substr($value, -1) == '"') {
							$value = substr(substr($value, 1), 0, -1);
						}
						
						if (strcasecmp($name, 'realm') == 0) {
							$this->_realm = $value;
						} else {
							if (isset($this->_params[$name])) {
								// WARNING HERE
								// we have already received parameter(s) with this name, so add to the list
								// of parameters with this name						
								if (is_scalar($this->_params[$name])) {
									// this is the first duplicate, so transform scalar (string) into an array
									// so we can add the duplicates
									$this->_params[$name] = array($this->_params[$name]);
								}
								
								$this->_params[$name][] = $value;
							} else {
								$this->_params[$name] = $value;
							}
						}
					}
				}
			}
		}		
	}		
	
	/**
	 * Parse the uri into its parts. Fill in the missing parts.
	 * 
	 * @todo  check for the use of HTTPS, right now we default to HTTP
	 * @todo  support for multiple occurrences of parameters
	 * @param string $params Optional extra parameters (from e.g the HTTP POST)
	 */
	protected function parseUri($params) {
		$components = parse_url($this->_uri);
				
		if (empty($components['scheme'])) {		// get the current/requested method
			$components['scheme'] = 'http';
		} else {
			$components['scheme'] = strtolower($components['scheme']);
		}
				
		if (empty($components['host'])) {		// get the current/requested host
			if (isset($_SERVER['HTTP_HOST'])) {
				$components['host'] = $_SERVER['HTTP_HOST'];
			} else {
				$components['host'] = '';
			}
		}
		
		// Phong Linh 2011/10/20 comment for dev pursposes
		//if (function_exists('mb_strtolower')) {
		//	$components['host'] = mb_strtolower($components['host']);
		//} else {
		//	$components['host'] = strtolower($components['host']);
		//}
		//
		//if (!preg_match('/^[a-z0-9\.\-]+$/', $components['host'])) {
		//	throw new Exception('Unsupported characters in host name');
		//}
		
		// get the port we are talking on
		if (empty($components['port'])) {
			$components['port'] = $this->getDefaultPort($components['scheme']);
		}
		
		if (empty($components['user'])) {
			$components['user'] = '';
		}
		
		if (empty($components['pass'])) {
			$components['pass'] = '';
		}
		
		if (empty($components['path'])) {
			$components['path'] = '/';
		}
		
		if (empty($components['query'])) {
			$components['query'] = '';
		}
		
		if (empty($components['fragment'])) {
			$components['fragment'] = '';
		}
		
		if(!is_array($this->_params)) {
			$this->_params = array();			
		}
		
		// now all is complete - parse all parameters
		foreach (array($components['query'], $params) as $inputs) {
			if (strlen($inputs) > 0) {
				$this->parseParameters($inputs);
			}
		}
		
		$this->_uriComponents = $components;
	}
	
	/**
	 * This function takes a input like a=b&a=c&d=e and returns the parsed
	 * parameters like this array('a' => array('b', 'c'), 'd' => 'e')
	 *
	 * @param string $input This is a description
	 * @return mixed Return the parameters
	 *
	 */
	protected function parseParameters($input) {
		if (!isset($input) || !$input) {
			return array();
		}
		
		if(strlen($input) > 0) {
			$pairs = explode('&', $input);
			foreach ($pairs as $pair) {
				if (strlen($pair) <= 0) {
					continue;
				}
				
				if (strpos($pair, '=')) {					
					@list($name, $value) = explode('=', $pair, 2);
					
					// check value exist
					$value = isset($value) ? $value : '';					
					if (isset($this->_params[$name])) {
						// we have already received parameter(s) with this name, so add to the list
						// of parameters with this name						
						if (is_scalar($this->_params[$name])) {
							// this is the first duplicate, so transform scalar (string) into an array
							// so we can add the duplicates
							$this->_params[$name] = array($this->_params[$name]);
						}
						
						$this->_params[$name][] = $value;
					} else {
						$this->_params[$name] = $value;
					}
				}
			}			
		}
		
		// return parsed parameters
		return $this->_params;
	}	
	
	/**
	 * Return the default port for a scheme
	 * 
	 * @param string $scheme Scheme of request HTTP or HTTPS
	 * @return int The default port associate to scheme
	 */
	protected function getDefaultPort($scheme) {
		switch ($scheme) {
			case 'http':
				return 80;
			
			case 'https':
				return 43;
			
			default:
				throw new OAuthException('Unsupported scheme type, expected http or https, got "' . $scheme . '"');
				break;
		}
	}	
	
	/**
	* Return the complete parameter string for the signature check.
	* All parameters are correctly URL encoded and sorted on name and value
	* 
	* @return string
	*/
	public function getNormalizedParams() {
		$params     = $this->_params;
		$normalized = array();
		
		ksort($params);
		foreach ($params as $key => $value) {
			// all names and values are already URL encoded, exclude the signature
			if ($key != self::$OAUTH_SIGNATURE) {
				if (is_array($value)) {
					$vals = $value;
					sort($vals);
					foreach ($vals as $v) {
						$normalized[] = $key . '=' . $v;
					}
				} else {
					$normalized[] = $key . '=' . $value;
				}
			}
		}
		
		return implode('&', $normalized);
	}
	
	/**
	 * Return the signature base string.
	 * Note that we can't use rawurlencode due to specified use of RFC3986.
	 * 
	 * @todo Get the request URL
	 * @todo Get the normalized parameters
	 * @return string The signature base string
	 */
	protected function getSignatureBase() {
		$sig	= array();
		$sig[]	= $this->_method;
		$sig[]	= $this->getRequestUrl();
		$sig[]	= $this->getNormalizedParams();
		
		return implode('&', array_map(array('Rfc3986', 'urlEncode'), $sig));
	}
	
	/**
	 * Fetch the signature object used for calculating and checking the signature base string
	 * 
	 * @param string method
	 * @return OAuthSignatureVerifier object
	 */
	function getSignatureVerifier($method) {
		$m     = strtoupper($method);
		$m     = preg_replace('/[^A-Z0-9]/', '_', $m);
		$class = 'OAuth' . $m . 'Verifier';

		if (class_exists($class)) {
			return new $class();
		} else {
			throw new OAuthException('Could not find class ' . $class);
		}
	}
	
	/**
	 * Perform version check.
	 * 
	 * @exception OAuthException thrown when sanity checks failed
	 */
	function checksVersion() {
		if (isset($this->param[self::$OAUTH_VERSION]))
		{
			$version = Rfc3986::urlDecode(self::$OAUTH_VERSION);
			if ($version != '1.0') {
				throw new OAuthException('Expected OAuth version 1.0, got "' . $this->param[self::$OAUTH_VERSION].'"');
			}
		}
	}	
}

?>